﻿///<reference path="../../Models/ParametricModel.ts"/>
///<reference path="../../Models/ProductModel.ts"/>

module Enferno.Public.Web.Components.Controllers.Base {

    export interface IOnProductInitilizedFunction {
        (productController: ProductControllerBase<ViewModels.Interfaces.IProductViewModel, ViewModels.Interfaces.IParametricValueViewModel, any>): void;
    }
    
    export class ProductControllerBase<TProducViewModel extends ViewModels.Interfaces.IProductViewModel, TParametricValueViewModel extends ViewModels.Interfaces.IParametricValueViewModel, TParametricValueHolder> {

        public static onInitializedFunctions = new Array<IOnProductInitilizedFunction>();

        public viewModel: TProducViewModel;
        public getOnHandStatusDisplayText: (text1: string, text2: string, count: number) => string;
        public priceFormator: (price: number) => string;

        public variantParametricIdsToPossibleValues: collections.Dictionary<number, Array<TParametricValueViewModel>>;
        private variantParametricIdsToSelectionChangedHandlers: collections.Dictionary<number, () => void>;
        private variantParametricIdsToSelectionOptions: collections.Dictionary<number, ParametricSelectionOptions>;
        private defaultParametricSelectionOptions: ParametricSelectionOptions;
        private variantParametricsRenderedSpecifically: collections.Set<number>;

        private baseDisplayPrice: number;
        private baseCatalogPrice: number;

        public init(data: any, parametricIdsRenderedExplicitly: Array<number>): void {
            var productModel = this.setDataFromServer(data);

            Enumerable.from(parametricIdsRenderedExplicitly).forEach(parametricId => this.registerParametricAsRenderedExplicitly(parametricId));

            Enumerable.from(ProductControllerBase.onInitializedFunctions)
                .reverse()
                .forEach((fn: IOnProductInitilizedFunction) => fn(this));

            if (productModel.variants.length == 1) {
                this.updateToVariant(productModel.variants[0]);
            }
            this.afterInitilized();
        }

        constructor() {
            this.variantParametricIdsToPossibleValues = new collections.Dictionary<number, Array<TParametricValueViewModel>>();
            this.variantParametricIdsToSelectionChangedHandlers = new collections.Dictionary<number, () => void>();
            this.variantParametricIdsToSelectionOptions = new collections.Dictionary<number, ParametricSelectionOptions>();
            this.variantParametricsRenderedSpecifically = new collections.Set<number>();
        }

        private setDataFromServer(serverData: any): Models.ProductModel {

            var productModel = new Models.ProductModel(serverData);
            this.baseDisplayPrice = productModel.displayPrice;
            this.baseCatalogPrice = productModel.catalogPrice;

            this.viewModel = this.createViewModel(productModel);

            //set up variantParametric map and variant parametric properties
            var variantParametrics = serverData.variantParametrics;
            if (this.hasVariantsButNoVariantParametrics()) {
                variantParametrics = this.getDefaultVariantParametrics();
            }

            Enumerable.from(variantParametrics).forEach((variantParametric: any) => {

                var selectedParametricViewModel = this.createParametricValueViewModel(null);
                this.viewModel.setVariantParametricSelectedValue<TParametricValueViewModel>(variantParametric.id, selectedParametricViewModel);

                var variantValues = new Array<TParametricValueViewModel>();
                Enumerable.from(variantParametric.values).forEach(variantParametricValueServerData => {
                    var parametricValueModel = new Models.ParametricValueModel(variantParametricValueServerData);
                    var parametricValueViewModel = this.createParametricValueViewModel(parametricValueModel);
                    variantValues.push(parametricValueViewModel);
                });

                this.variantParametricIdsToPossibleValues.setValue(variantParametric.id, variantValues);
            });

            this.viewModel.setDefaultImage(this.getDefaultImage());
            this.viewModel.setAdditionalImages(this.getAdditionalImages());

            return productModel;
        }

        public registerParametricSelectionChangedHandler(parametricId: number, handler: () => {}): void {
            this.variantParametricIdsToSelectionChangedHandlers.setValue(parametricId, handler);
        }

        private registerParametricAsRenderedExplicitly(parametricId: number) {
            this.variantParametricsRenderedSpecifically.add(parametricId);
        }

        public getVariantParametricsToRender(): Array<Models.ParametricModel> {

            var genericParametrics = Enumerable.from(this.viewModel.getVariantParametrics())
                .where((parametric: Models.ParametricModel) => {
                    return !this.variantParametricsRenderedSpecifically.contains(parametric.id);
                })
                .toArray();

            if (this.hasVariantsButNoVariantParametrics())
                genericParametrics = this.getDefaultVariantParametrics();

            return genericParametrics;
        }

        public registerDefaultParametricsSelectionOptions(
            domNodesSelector: () => Array<Element>,
            onIsSelectable: (domNode: Element) => void,
            onIsNotSelectable: (domNode: Element) => void,
            onSetSelected: (domElement: Element, selected: boolean) => void,
            valueExtractor?: (domNode: Element) => string,
            idExtractor?: (domNode: Element) => number) {

            var options = new ParametricSelectionOptions();
            options.domNodesSelector = domNodesSelector;
            options.onIsSelectable = onIsSelectable;
            options.onIsNotSelectable = onIsNotSelectable;
            options.valueExtractor = valueExtractor;
            options.idExtractor = idExtractor;
            options.onSetSelected = onSetSelected;

            this.defaultParametricSelectionOptions = options;

        }

        public registerParametricSelectionOptions(parametricId: number, domNodesSelector: () => Array<Element>,
            onIsSelectable: (domNode: Element) => void, onIsNotSelectable: (domNode: Element) => void, onSetSelected: (domElement: Element, selected: boolean) => void,
            valueExtractor?: (domNode: Element) => string,
            idExtractor?: (domNode: Element) => number) {

            var parametricSelectionOptions = new ParametricSelectionOptions();
            parametricSelectionOptions.domNodesSelector = domNodesSelector;
            parametricSelectionOptions.onIsSelectable = onIsSelectable;
            parametricSelectionOptions.onIsNotSelectable = onIsNotSelectable;
            parametricSelectionOptions.valueExtractor = valueExtractor;
            parametricSelectionOptions.idExtractor = idExtractor;
            parametricSelectionOptions.onSetSelected = onSetSelected;

            this.variantParametricIdsToSelectionOptions.setValue(parametricId, parametricSelectionOptions);
        }

        public getValuesForVariantParametric(parametricId: number): Array<TParametricValueViewModel> {
            var values = this.variantParametricIdsToPossibleValues.getValue(parametricId);

            if (this.hasVariantsButNoVariantParametrics()) {
                //setup default variant parametrics
                var defaultVaraintParametrics = this.getDefaultVariantParametrics();
                var matchingParametric = Enumerable.from(defaultVaraintParametrics).single(parametric => parametric.id == parametricId);

                values = matchingParametric.values;
            }
            return values;
        }

        public getSelectedValueHolderForVariantParametric(parametricId: number): TParametricValueHolder {
            var value = this.viewModel.getVariantParametricSelectedValueHolder<TParametricValueHolder>(parametricId);
            return value;
        }

        public setSelectedValueForParametric(parametricId: number, parametricValue: TParametricValueViewModel): void {
            this.viewModel.setVariantParametricSelectedValue<TParametricValueViewModel>(parametricId, parametricValue);
        }

        public getPriceFormattedString(price: number): string {
            if (this.priceFormator != null)
                return this.priceFormator(price);
            return price.toString();
        }

        private getDefaultVariantParametrics(variantId?: number): Array<Models.ParametricModel> {
            var defaultVariantParametric = new Models.ParametricModel();
            //TODO: these are hardcoded now. Should be parameters.
            defaultVariantParametric.id = -1;
            defaultVariantParametric.name = "Variant";

            var variants = this.viewModel.getVariants();

            var variantsToGenerateParametricsFrom = variants;

            if (variantId != null) {
                variantsToGenerateParametricsFrom = Enumerable.from(variants).where(variant => variant.id == variantId).toArray();
            }

            var values = Enumerable.from(variantsToGenerateParametricsFrom)
                .select(variant => {
                    var value = new Models.ParametricValueModel({ value: variant.name, id: 0, sortOrder: 0 });
                    return value;
                }).toArray();

            defaultVariantParametric.values = values;
            return new Array<Models.ParametricModel>(defaultVariantParametric);
        }

        private hasVariantsButNoVariantParametrics(): boolean {
            var variants = this.viewModel.getVariants();
            var variantParametrics = this.viewModel.getVariantParametrics();
            return variants.length > 1 && variantParametrics.length == 0;
        }

        public createViewModel(productModel: Models.ProductModel): TProducViewModel {
            throw "Class is abstract. Implement in concrete class";
        }

        public createParametricValueViewModel(model: Models.ParametricValueModel): TParametricValueViewModel {
            throw "Class is abstract. Implement in concrete class";
        }

        public afterInitilized() {
            throw "Class is abstract. Implement in concrete class";
        }

        public setVariantParametricValue(parametricId: number, parametricValue: Models.ParametricValueModel): void {

            //check all variants and find the first one with a matching variant parametric value
            var variantsWithValue = this.getVariantsWithValue(parametricId, parametricValue);

            if (variantsWithValue.length > 0) {
                //find the variant that matches all of the set variant parametrics
                var matchingVariant: Models.ProductVariantModel = Enumerable.from(variantsWithValue).first((variant: Models.ProductVariantModel) => {
                    var isMatch = true;

                    var variantParametricIds = this.viewModel.getVariantParametricSelectedIds();

                    Enumerable.from(variantParametricIds).forEach(key => {
                        var variantParametricValue = this.viewModel.getVariantParametric(key);

                        //var selectedValueHolder: TParametricValueHolder = this.viewModel.getVariantParametricSelectedValueHolder<TParametricValueHolder>(key);
                        //var selectedValue: TParametricValueViewModel = this.viewModel.getValueForSelectedValueHolder<TParametricValueHolder, TParametricValueViewModel>(selectedValueHolder);
                        
                        if (variantParametricValue != null) {

                            var currentlySelectedValue: any = this.viewModel.getSelectedValueForVariantParametric<TParametricValueViewModel>(key);
                            var value: TParametricValueViewModel = null;

                            if (typeof currentlySelectedValue === "string") {
                                //assume we have a value. Find the parametric value with the correspronding value.
                                //Note! We have to do this since when we create default variant parametrics we get a string here and not a ParametricValue object. Can't understand why...
                                //var val = currentlySelectedValue.getValue();
                                value = this.getVariantParametricValueWithValue(key, <string>currentlySelectedValue);
                            } else { //assume that the value is of type ParametricValue already.
                                value = currentlySelectedValue;
                            }

                            var variantParametric: Models.ParametricModel = Enumerable.from(variant.variantParametrics).single((parametric: Models.ParametricModel) => parametric.id == key);
                            var variantValue = variantParametric.values[0]; //also, here we should support multiple values
                            if (!value.toModel().equals(variantValue)) {
                                isMatch = false;
                                return;
                            }
                        }
                    });

                    return isMatch;
                });

                var activeVariant = this.viewModel.getActiveVariant();

                if (activeVariant == null || matchingVariant.id != activeVariant.id) {
                    this.updateToVariant(matchingVariant, parametricId, parametricValue);
                }
            }

        }

        private getVariantParametricValueWithValue(parametricId: number, value: string): TParametricValueViewModel {
            var possibleValues = this.variantParametricIdsToPossibleValues.getValue(parametricId);
            return Enumerable.from(possibleValues).single((parametricValue: TParametricValueViewModel) => parametricValue.getValue() == value);
        }

        private getVariantsWithValue(parametricId: number, parametricValue: Models.ParametricValueModel): Array<Models.ProductVariantModel> {
            var variants = this.viewModel.getVariants();
            if (this.hasVariantsButNoVariantParametrics()) {
                //create default variant parametrics.
                Enumerable.from(variants).forEach(variant => {
                    var defaultVariantParametrics = this.getDefaultVariantParametrics(variant.id);
                    variant.variantParametrics = defaultVariantParametrics;
                });
            }

            var variantsWithValue: Array<Models.ProductVariantModel> = Enumerable.from(variants).where((variant: Models.ProductVariantModel) => {

                var parametricForId = Enumerable.from(variant.variantParametrics).single(parametric => parametric.id == parametricId);

                //todo, we should support parametrics with multiple values
                if (parametricValue.id != null && parametricValue.id != 0 && parametricForId.values.length == 1 && parametricForId.values[0].id == parametricValue.id) { //if we got the value id, compare with that.
                    return true;
                } else if (parametricValue.value != null && parametricForId.values.length == 1 && parametricForId.values[0].value == parametricValue.value) { //else, if we got the value, compare with that
                    return true;
                }

                return false;
            }).toArray();

            //if no variants where found, return all
            if (variantsWithValue.length == 0)
                return this.viewModel.getVariants();

            return variantsWithValue;
        }

        public resetSelectedVariant(): void {
            //reset selected values
            var ids = this.viewModel.getVariantParametricSelectedIds();
            Enumerable.from(ids)
                .forEach(variantParametricKey => {
                this.viewModel.setVariantParametricSelectedValue<TParametricValueViewModel>(variantParametricKey, null);
            });

            //set all values as selectable.
            Enumerable.from(this.variantParametricIdsToPossibleValues.keys())
                .forEach(variantParametricKey => {
                    var possibleValues = this.variantParametricIdsToPossibleValues.getValue(variantParametricKey);
                    Enumerable.from(possibleValues).forEach((parametricValue: TParametricValueViewModel) => parametricValue.setIsSelectable(true));
                    this.onParametricChanged(variantParametricKey);
                });

            this.updatePrice();
            this.updateImages();

            this.viewModel.setPartNo(null);
            this.viewModel.setPriceListId(null);
            this.viewModel.setOnHandStatusDisplayText(null);
            this.viewModel.setActiveVariant(null);
        }

        private updateToVariant(variant: Models.ProductVariantModel, parametricId?: number, parametricValue?: Models.ParametricValueModel) {
            this.updatePrice(variant);
            this.updateImages(variant);
            this.updateOnHandDisplayText(variant);

            this.viewModel.setPartNo(variant.partNo);
            this.viewModel.setPriceListId(variant.priceListId);

            this.viewModel.setActiveVariant(variant);

            if (parametricId != null) {
                this.updateParametricSelection(parametricId, parametricValue);
            }
        }

        private updatePrice(variant?: Models.ProductVariantModel): void {
            if (variant != null) {
                this.viewModel.setDisplayPrice(variant.displayPrice);
                this.viewModel.setCatalogPrice(variant.catalogPrice);
            } else {
                this.viewModel.setDisplayPrice(this.baseDisplayPrice);
                this.viewModel.setCatalogPrice(this.baseCatalogPrice);
            }
        }

        private updateImages(variant?: Models.ProductVariantModel): void {
            this.viewModel.setDefaultImage(this.getDefaultImage());
            this.viewModel.setAdditionalImages(this.getAdditionalImages());
        }

        private updateOnHandDisplayText(variant?: Models.ProductVariantModel): void {
            if (variant != null) {
                if (this.getOnHandStatusDisplayText != null) {
                    this.viewModel.setOnHandStatusDisplayText(this.getOnHandStatusDisplayText(variant.onHandStatusText1, variant.onHandStatusText2, variant.onHandStatusCount));
                } else {
                    this.viewModel.setOnHandStatusDisplayText(variant.onHandStatusText1);
                }
            } else {
                this.viewModel.setOnHandStatusDisplayText(null);
            }
        }

        private getDefaultImage(): Models.FileInfoModel {

            var defaultImage: Models.FileInfoModel = null;
            var activeVariant = this.viewModel.getActiveVariant();
            if (activeVariant != null) {
                defaultImage = Enumerable.from(activeVariant.files).singleOrDefault(file => file.type == Models.FileType.VariantImage && file.url != null);
            }

            var productFiles = this.viewModel.getAllFiles();
            if (defaultImage == null)
                defaultImage = Enumerable.from(productFiles).single(file => file.type == Models.FileType.DefaultImage);

            return defaultImage;
        }

        private getAdditionalImages(): Array<Models.FileInfoModel> {
            var additionalImages: Array<Models.FileInfoModel> = null;
            var activeVariant = this.viewModel.getActiveVariant();
            var productFiles = this.viewModel.getAllFiles();

            if (activeVariant != null) {
                
                var foundImages = Enumerable.from(productFiles).where(file => file.type == Models.FileType.AdditionalImage).toArray();
                if (foundImages.length > 0)
                    additionalImages = foundImages;
            }

            if (additionalImages == null) {
                additionalImages = Enumerable.from(productFiles).where(file => file.type == Models.FileType.AdditionalImage).toArray();
            }

            return additionalImages;
        }

        private updateParametricSelection(parametricId: number, parametricValue: Models.ParametricValueModel) {
            var variantsWithValue = this.getVariantsWithValue(parametricId, parametricValue);

            var variantParametricIdsToSelectableValues = new collections.Dictionary<number, Array<TParametricValueViewModel>>();

            //find all values for the variants that had the given value and record them for later.
            Enumerable.from(variantsWithValue).forEach((variant: Models.ProductVariantModel) => {
                Enumerable.from(variant.variantParametrics).forEach((variantParametric: Models.ParametricModel) => {
                    var valueList = variantParametricIdsToSelectableValues.getValue(variantParametric.id);
                    if (valueList == null) {
                        valueList = new Array<TParametricValueViewModel>();
                    }
                    //TODO: support multiple parametric values
                    var valueToAdd = variantParametric.values[0];
                    var containsValue = Enumerable.from(valueList).any(val => val.equals(valueToAdd));
                    if (!containsValue) {
                        var viewModel = this.createParametricValueViewModel(valueToAdd);
                        valueList.push(viewModel);
                        variantParametricIdsToSelectableValues.setValue(variantParametric.id, valueList);
                    }
                });
            });

            Enumerable.from(this.variantParametricIdsToPossibleValues.keys()).forEach((possibleVariantParametricKey: number) => {
                var possibleVariantParametricValues = this.variantParametricIdsToPossibleValues.getValue(possibleVariantParametricKey);

                var activeVariant = this.viewModel.getActiveVariant();
                var activeVariantParametric: Models.ParametricModel = Enumerable.from(activeVariant.variantParametrics)
                    .single((variantParametric: Models.ParametricModel) => variantParametric.id == possibleVariantParametricKey);

                var valueThatShouldBeSelected = activeVariantParametric.values[0];

                if (possibleVariantParametricKey != parametricId) { //only disable values for the parameters not choosen.
                    var selectableValues = variantParametricIdsToSelectableValues.getValue(possibleVariantParametricKey);

                    Enumerable.from(possibleVariantParametricValues).forEach((possibleValue: TParametricValueViewModel) => {
                        
                        if (!Enumerable.from(selectableValues).any((selectableValue: TParametricValueViewModel) => selectableValue.toModel().equals(possibleValue.toModel())
                        )) {
                            possibleValue.setIsSelectable(false);
                        } else {
                            possibleValue.setIsSelectable(true);
                        }
                    });
                }

                var parametricValueViewModel = this.createParametricValueViewModel(valueThatShouldBeSelected);
                this.onParametricChanged(possibleVariantParametricKey, parametricValueViewModel);
            });


        }

        private onParametricChanged(parametricId: number, valueThatShouldBeSelected?: TParametricValueViewModel): void {

            //call changedHandler if any
            var changedHandler = this.variantParametricIdsToSelectionChangedHandlers.getValue(parametricId);
            if (changedHandler != null) {
                changedHandler();
            }

            var parametricSelectionOptions = this.variantParametricIdsToSelectionOptions.getValue(parametricId);

            if (parametricSelectionOptions == null) {
                if (this.defaultParametricSelectionOptions != null)
                    parametricSelectionOptions = this.defaultParametricSelectionOptions;
                else
                    return;
            }

            var domElements = parametricSelectionOptions.domNodesSelector(parametricId);
            var parametricValues = this.getValuesForVariantParametric(parametricId);

            Enumerable.from(domElements).forEach((domElement: Element) => {
                var parametricValue: TParametricValueViewModel = null;

                if (parametricSelectionOptions.valueExtractor != null) {
                    var value = parametricSelectionOptions.valueExtractor(domElement);
                    parametricValue = Enumerable.from(parametricValues).single((val: TParametricValueViewModel) => val.getValue() == value);
                } else if (parametricSelectionOptions.idExtractor != null) {
                    var id = parametricSelectionOptions.idExtractor(domElement);
                    parametricValue = Enumerable.from(parametricValues).single((val: TParametricValueViewModel) => val.getId() == id);
                } else {
                    throw "Neither valueExtractor nor idExtractor is available. One of them must be specified!";
                }

                var isSelectable = parametricValue.getIsSelectable();
                if (!isSelectable) {
                    parametricSelectionOptions.onIsNotSelectable(domElement);
                } else {
                    parametricSelectionOptions.onIsSelectable(domElement);
                }

                if (valueThatShouldBeSelected == null) {
                    parametricSelectionOptions.onSetSelected(domElement, false);
                } else {
                    var isEqual = valueThatShouldBeSelected.toModel().equals(parametricValue.toModel());
                    parametricSelectionOptions.onSetSelected(domElement, isEqual);
                }
            });
        }
    }

    class ParametricSelectionOptions {
        public domNodesSelector: (parametricId: number) => Array<Element>;
        public valueExtractor: (domNode: Element) => string;
        public idExtractor: (domNode: Element) => number;
        public onIsSelectable: (domNode: Element) => void;
        public onIsNotSelectable: (domNode: Element) => void;
        public onSetSelected: (domNode: Element, selected: boolean) => void;
    }

}
